package va;

import btree.*;
import bufmgr.HashEntryNotFoundException;
import bufmgr.InvalidFrameNumberException;
import bufmgr.PageUnpinnedException;
import bufmgr.ReplacerException;
import global.RID;
import global.Vector100Dtype;
import heap.*;
import va_file.VAFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;

public class BaseVAScan {

    public Scan fileScan;
    public BTFileScan btScan;

    //Get next entry from the VAFile.
    public VAEntry getNextVA(int numberOfBits) throws Exception {
        if (fileScan != null) {
            return this.getNextHeapVA();
        } else if (btScan != null) {
            return this.getNextBTreeVA(numberOfBits);
        } else {
            throw new Exception("Scanner not initialized");
        }
    }

    // Get the next entry from the heap-based VA file
    public VAEntry getNextHeapVA() throws IOException, InvalidTupleSizeException {
        Tuple tuple = fileScan.getNext(new RID());
        if (tuple == null) return null;
        byte[] bytes = tuple.getTupleByteArray();
        return VAEntry.fromBytes(bytes);
    }

    // Get the next entry from the btree-based VA file
    public VAEntry getNextBTreeVA(int numberOfBits) throws ScanIteratorException {
        KeyDataEntry nextEntry = btScan.get_next();
        if (nextEntry == null) return null;
        RID rid = ((LeafData) nextEntry.data).getData(); //get the RID, which is the leaf data
        StringKey sk = ((StringKey) nextEntry.key); //get the key
        String key = sk.getKey();
        BitSet approximation = VAEntry.CreateApproximationFromIndexString(key, numberOfBits);
        return new VAEntry(approximation, rid);
    }

    // This function calculates the minimum distance to every range for a target vector
    // The idea is to cache them all at the beginning so this isn't called every single time
    // in the RangeScan function
    public static short[][] GetMinDistances(Range[] ranges, Vector100Dtype target) {
        int numRanges = ranges.length;
        short[][] result = new short[100][numRanges];
        for (int i = 0; i < 100; i++) {
            for (int j = 0; j < numRanges; j++) {
                result[i][j] = Range.GetMinPossibleDistance(ranges[j], target.getIndex(i));
            }
        }
        return result;
    }

    /* Takes a Vector100Dtype object as an argument and find the possible candidates (RIDs) within the given
	 range (distance) by computing Euclidean distance between the target object and all the vector points in the
	 VA Index file. This function returns a list of RIDs as an ArrayList.
	*/
    public ArrayList<RID> VARangeScan(Vector100Dtype target, int distance, int noOfBits) throws HFException, HFBufMgrException,
            HFDiskMgrException, IOException
    {
        ArrayList<RID> rid = new ArrayList<RID>();
        BitSet dimension;
        int decNum=0;
        int i=0,j=0;
        int dest=100*noOfBits;
        Range[] ranges = VAFile.GetDefaultRanges(noOfBits);
        short[][] rangeMinDistances = GetMinDistances(ranges, target);
        double distSquared = Math.pow(distance, 2);
        try
        {
            VAEntry entry = this.getNextVA(noOfBits);
            while(entry != null)
            {
			/*
			 * The idea here is that we are calculating the Euclidean distance as we go. If the
			 * total distance up to this point has crossed the maximum Euclidean distance for the
			 * desired range, just break and don't add it to the ArrayList. This significantly saves
			 * computation of the RIDs when the range is small.
			 */
                boolean add = true;
                double currentDistance = 0;
                for(i=0,j=0;i<dest && j<100;i=i+noOfBits,j++)
                {
                    dimension = entry.approximation.get(i,i+noOfBits);
                    decNum = dimension.toInt(noOfBits);

                    currentDistance += Math.pow(target.getIndex(j) - rangeMinDistances[j][decNum], 2);
                    if (currentDistance > distSquared) {
                        add = false;
                        break;
                    }

                }

                if (add) {
                    rid.add(entry.rid);
                }

                entry = this.getNextVA(noOfBits);
            }
        }
        catch (Exception e ) {
            e.printStackTrace();
            throw new IOException();
        }
        return rid;
    }
    

    /* Takes a Vector100Dtype object as an argument and find the possible K nearest neighbor candidates (RIDs) by computing
	 Euclidean distance between the target object and all the vector points in the
	 VA Index file. This function returns a list of RIDs as an ArrayList.
	*/
    public ArrayList<RID> VANNScan(Vector100Dtype target, int count, int noOfBits) throws HFException, HFBufMgrException,
            HFDiskMgrException, IOException
    {
        ArrayList<RID> rid=new ArrayList<RID>();
        lboundRID tempObject=new lboundRID();
        ArrayList<lboundRID> lbrid=new ArrayList<lboundRID>();
        ArrayList<Double> uboundDistance=new ArrayList<Double>();
        Vector100Dtype vdMin = new Vector100Dtype();
        Vector100Dtype vdMax = new Vector100Dtype();
        BitSet dimension;
        int decNum=0;
        int i=0,j=0;
        double k=0;
        int dest=100*noOfBits;
        Range[] ranges = VAFile.GetDefaultRanges(noOfBits);
        try
        {
            VAEntry entry = this.getNextVA(noOfBits);
            if(entry == null)
                return null;
            while(entry != null)
            {
                for(i=0,j=0;i<dest && j<100;i=i+noOfBits,j++)
                {
                    dimension=entry.approximation.get(i,i+noOfBits);
                    decNum=dimension.toInt(noOfBits);
                    Range desiredRange = ranges[decNum];
                    int targetValue = target.getIndex(j);
                    vdMin.setIndex(j,Range.GetMinPossibleDistance(desiredRange, targetValue));
                    vdMax.setIndex(j,Range.GetMaxPossibleDistance(desiredRange, targetValue));
                }
                tempObject = new lboundRID();
                tempObject.lboundDistance=Vector100Dtype.Distance(target,vdMin);
                tempObject.rid=entry.rid;
                lbrid.add(tempObject);
                uboundDistance.add(Vector100Dtype.Distance(target,vdMax));
                entry = this.getNextVA(noOfBits);
            }
            Collections.sort(uboundDistance);
            k=uboundDistance.get(count-1);
            for(i=0;i<lbrid.size();i++)
            {
                tempObject=lbrid.get(i);
                if(tempObject.lboundDistance<=k)
                {
                    rid.add(tempObject.rid);
                }
            }
        }
        catch (Exception e ) {
            throw new IOException();
        }
        return rid;
    }

    /*
     * Closes the scanners associated with the object to avoid leaving buffer pages pinned
     */
    public void close() throws PageUnpinnedException, InvalidFrameNumberException, HashEntryNotFoundException, ReplacerException, IOException {
        if (this.fileScan != null) {
            this.fileScan.closescan();
        }

        if (this.btScan != null) {
            this.btScan.DestroyBTreeFileScan();
        }
    }

    class lboundRID
    {
        public double lboundDistance;
        public RID rid;
    }

}
